package org.infodavid.common.persistence.impl;

import java.util.List;

import javax.persistence.PersistenceException;
import javax.persistence.Query;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.Type;
import org.infodavid.common.persistence.IQueryParameter;
import org.infodavid.common.persistence.IQueryParameter.EParameterType;
import org.infodavid.common.persistence.PersistenceSession;

/**
 * The Class HibernateHelper.
 */
final public class HibernateHelper {

  /** logger. */
  private static final Log LOG = LogFactory.getLog(HibernateHelper.class);

  /** The Constant INSTANCE. */
  private static final HibernateHelper INSTANCE = new HibernateHelper();

  /**
   * Gets the instance.
   * @return the instance
   */
  public static HibernateHelper getInstance() {
    return INSTANCE;
  }

  /** The is debug enabled. */
  private final boolean isDebugEnabled = LOG.isDebugEnabled();

  /**
   * The Constructor.
   */
  private HibernateHelper() {
    super();
  }

  /**
   * Apply parameters.
   * @param criteria the criteria
   * @param definition the definition of hints, separated using ';'. Example:
   *          hint1=value1;hint2=value2
   * @throws PersistenceException the exception
   */
  public void applyHints(final Criteria criteria, final String definition)
      throws PersistenceException {
    throw new PersistenceException("Not implemented");
  }

  /**
   * Apply parameters.
   * @param params the parameters
   * @param criteria the criteria
   * @throws PersistenceException the exception
   */
  public void applyParameters(final List<IQueryParameter> params, final Criteria criteria)
      throws PersistenceException {
    if (params == null || params.isEmpty()) {
      if (isDebugEnabled) {
        LOG.debug("No parameter to apply");
      }
    }
    else {
      if (isDebugEnabled) {
        LOG.debug("Applying parameter(s): " + params.size());
      }

      for (final IQueryParameter param : params) {
        if (EParameterType.FIRST_ROW == param.getType()) {
          if (isDebugEnabled) {
            LOG.debug("Applying first row parameter: " + param.getValue());
          }

          criteria.setFirstResult(((Number)param.getValue()).intValue());
        }
        else if (EParameterType.MAX_ROWS == param.getType()) {
          if (isDebugEnabled) {
            LOG.debug("Applying max rows parameter: " + param.getValue());
          }

          criteria.setMaxResults(((Number)param.getValue()).intValue());
        }
        else if (IQueryParameter.EParameterType.HINTS == param.getType()) {
          applyHints(criteria, param.getValue().toString());
        }
        else {
          final EParameterType type = param.getType();
          final String name = param.getColumnName();

          try {
            if (type == EParameterType.ORDER_ASCENDING) {
              if (StringUtils.isEmpty(name)) {
                throw new IllegalArgumentException("Parameter name is null or invalid: " + name);
              }

              criteria.addOrder(Order.asc(name));
            }
            else if (type == EParameterType.ORDER_DESCENDING) {
              if (StringUtils.isEmpty(name)) {
                throw new IllegalArgumentException("Parameter name is null or invalid: " + name);
              }

              criteria.addOrder(Order.desc(name));
            }
            else {
              try {
                criteria.add(Restrictions.eq(param.getName(), param.getValue()));
              }
              catch (final Exception e) {
                LOG.warn("Invalid query parameter: " + param.getName(), e);
              }
            }
          }
          catch (final Exception e) {
            throw new IllegalArgumentException("Invalid query parameter: " + param.getName(), e);
          }
        }
      }
    }
  }

  /**
   * Apply parameters.
   * @param query the query
   * @param definition the definition of hints, separated using ';'. Example:
   *          hint1=value1;hint2=value2
   */
  public void applyHints(final Query query, final String definition) {
    final String[] hints = definition.split(";");
    String[] parts;

    for (final String hint : hints) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Applying hint: " + hint);
      }

      parts = hint.split("=");
      query.setHint(parts[0], parts[1]);
    }
  }

  /**
   * Apply parameters.
   * @param params the parameters
   * @param query the query
   */
  public void applyParameters(final List<IQueryParameter> params, final Query query) {
    if (params == null || params.isEmpty()) {
      if (isDebugEnabled) {
        LOG.debug("No parameter to apply");
      }
    }
    else {
      if (isDebugEnabled) {
        LOG.debug("Applying parameter(s): " + params.size());
      }

      for (final IQueryParameter param : params) {
        if (EParameterType.FIRST_ROW == param.getType()) {
          if (isDebugEnabled) {
            LOG.debug("Applying first row parameter: " + param.getValue());
          }

          query.setFirstResult((int)Double.parseDouble(param.getValue().toString()));
        }
        else if (EParameterType.MAX_ROWS == param.getType()) {
          if (isDebugEnabled) {
            LOG.debug("Applying max rows parameter: " + param.getValue());
          }

          query.setMaxResults((int)Double.parseDouble(param.getValue().toString()));
        }
        else if (EParameterType.HINTS == param.getType()) {
          applyHints(query, param.getValue().toString());
        }
        else if (EParameterType.ORDER_ASCENDING == param.getType()) {
          LOG.warn("Order specified for a JPA query: " + param);
        }
        else if (EParameterType.ORDER_DESCENDING == param.getType()) {
          LOG.warn("Order specified for a JPA query: " + param);
        }
        else {
          try {
            query.setParameter(param.getName(), param.getValue());
          }
          catch (final Exception e) {
            LOG.warn("Invalid query parameter: " + param.getName(), e);
          }
        }
      }
    }
  }

  /**
   * Gets the session.
   * @param value the value
   * @return the session
   * @throws PersistenceException the persistence exception
   */
  public Session getSession(final PersistenceSession value) throws PersistenceException {
    return (Session)value.getDelegate();
  }

  /**
   * Creates the criteria.
   * @param value the value
   * @param persistentClass the persistent class
   * @return the criteria
   * @throws PersistenceException the persistence exception
   */
  @SuppressWarnings("rawtypes")
  public Criteria createCriteria(final PersistenceSession value, final Class persistentClass)
      throws PersistenceException {
    return getSession(value).createCriteria(persistentClass);
  }

  /**
   * Creates the query.
   * @param value the value
   * @param hql the query statements
   * @return the org.hibernate. query
   * @throws PersistenceException the persistence exception
   */
  public org.hibernate.Query createQuery(final PersistenceSession value, final String hql)
      throws PersistenceException {
    return getSession(value).createQuery(hql);
  }

  /**
   * SQL restriction.
   * @param sql the SQL query
   * @param mark the mark used to place the binding variables or question marks
   * @param values the values
   * @param type the type of the values
   * @return the criterion
   */
  public Criterion sqlRestriction(final String sql, final String mark, final Object[] values,
      final Type type) {
    final org.hibernate.type.Type[] types = new org.hibernate.type.Type[values.length];
    final StringBuilder bindings = new StringBuilder();

    bindings.append("(?");
    types[0] = type;

    for (int i = 1; i < values.length; i++) {
      bindings.append(",");
      bindings.append("?");
      types[i] = type;
    }

    bindings.append(')');

    return Restrictions.sqlRestriction(sql.replace(mark, bindings.toString()), values, types);
  }
}
